<?php
/**
 */
class Yz_Action_Select
    extends Yz_Action_Abstract
{

    const FJOIN = '_';

    /**
     *  @var array( fieldName: fieldSelectClause )
     */
    protected $fields = array();

    /** SQL rows limit, set to 1 for base Select to avoid occasionally selecting all rows
     *  @var int(1)
     */
    protected $limit = 1;

    protected $fieldStack = array();
    protected $substAlias = array();
    protected $inField;
    protected $result = array();

    /************************************
     *          Public methods          *
     ************************************/


    /**
     *  @return Yz_Hash
     */
    public function fetch()
    {
        if (null === $this->count)
            $this->perform();
        $values = $this->statement->fetch();
        assert('!!$values');

        $this->__dict__ = $values->toArray();
        $this->count = null; // after yielding result, enable execution anew
        return $this;
    }



    /************************************
     *        Protected methods         *
     ************************************/

    /**
     *  Apply the arguments to the SQL statement
     *      Setup limit to avoid occasional selection of all rows
     *  @param Yz_SQL_Statement $sql
     *  @return this
     */
    protected function applyArguments(Yz_SQL_Statement $sql)
    {
        debug();
        if ($this->limit)
            $sql->limit($this->limit);
        return $this;
    }

    /**
     *  Common SELECT statement creator:
     *      the common part of statement
     *  @return Yz_SQL_Statement_Select
     */
    protected function buildStatement()
    {
        debug();
        //$this->result = new Yz_Hash();
        $this->inspectModel();
        return Yz_SQL_Statement_Select::create()
            ->select($this->fields)
            ->from($this->tables)
        ;
    }



    /**
     *  Inspect model's property and create a field clause or
     *      recursively inspect referenced model
     *  @param Yz_Model_Interface $model
     *  @param string $alias
     *  @param string $propName
     *  @param Yz_Model_Field_Interface $prop
     *  @return this
     */
    protected function inspectField(
            Yz_Model_Interface $model,
            $alias,
            $propName,
            Yz_Model_Field_Interface $prop)
    {
        debug("Field: {$alias}.{$propName}");

        $f = $this->enterField($propName);

        if ($prop instanceof Yz_Model_Field_Reference) {
            #$this->result[$f] = $prop;
            $clause = $this->inspectReferenceField($model, $alias, $propName, $prop);
        } else {

            // always generate an entry for a field
            // TODO check custom settings for the field
            $clause = "{$alias}.{$propName}";
        }
        $this->fields[$f] = $clause;
        $this->exitField();
        return $this;
    }

    /**
     *  @return string field clause
     */
    protected function inspectReferenceField(
            Yz_Model_Interface $origModel,      // original model (to which the field belongs)
            $modelAlias,                        // original model's alias
            $propName,
            Yz_Model_Field_Interface $prop)
    {

        $refModel = $prop->getModel();
        #debug("Ref Field: {$modelAlias}.{$propName} -> {$refModel}");

        $refAlias = $this->inField;
        $fieldAlias = $refAlias;

        $recurse = true;
        if ($origModel === $refModel) { // aka SelfReference
            #$keyName = $propName . 'Id';
            $recurse = false;           // do not join on self
            // add the field as if it belonged to the referenced model
            $fieldClause = $modelAlias . '.' . $propName . 'Id';
        } else if ($prop instanceof Yz_Model_Field_OneToOne) { // including Field_ParentModel
            // models are joined by id
            // add two field clauses: for "my" id and for "parent's" id
            // which have the same values
            // e.g. MyModel.id AS MyModel_UpModel, ParentModel.id AS ParentModel_id
            $keyName = 'id'; // join on id
            $fieldClause = $modelAlias . '.' . $keyName;

            // add field to select list
            #$this->fields[$fieldAlias] = $modelAlias . '.' . $keyName; // e.g. MyModel.parentId AS MyModel_parent
        } else {
            // the property is a foreign key
            $keyName = $propName . 'Id';
            $fieldClause = $modelAlias . '.' . $propName . 'Id';
        }

        if ($recurse) {
            $this
                ->addSource($origModel, $modelAlias, $keyName, $refModel, $refAlias)
                ->doInspectModel($refModel, $refAlias)
            ;
        }
        return $fieldClause;
    }



    /** Add a table to table list using info from property
     *  @param $origModel, $origField, $refModel
     *  NOTE: the resulting origField will have 'Id' added (eg. type -> typeId)
     */
    protected function addSource($origModel, $origAlias, $origField, $refModel, $refAlias)
    {
        debug("Add {$refModel} as {$refAlias}");
        $key       = 'id';                          // use referenced model's primary key field name
        $refTable  = $refModel->getSourceName();       // ref.model table name
        // TODO
        //  if prop.allowNull : use LEFT JOIN
        //  otherwise, use INNER JOIN
        $table = Yz_SQL_Join::leftJoin($refTable, $refAlias)
            ->joinOn("{$refAlias}.{$key}={$origAlias}.{$origField}")
        ;
        $this->tables[$refAlias] = $table;

        return $this;
    }


    /** Enter a property into stack
     *
     */
    protected function enterField($propName)
    {
        $this->fieldStack[] = $propName;
        $this->inField = $f = join(self::FJOIN, $this->fieldStack);
        return $f;
    }

    /** Pop last entered property off stack
     *
     */
    protected function exitField()
    {
        array_pop($this->fieldStack);
        return $this;
    }

}
